/***************************************************************************************
* Copyright (c) 2014-2022 Zihao Yu, Nanjing University
*
* NEMU is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*          http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
*
* See the Mulan PSL v2 for more details.
***************************************************************************************/

#include <common.h>
#include <device/map.h>
#include <SDL2/SDL.h>
#include <fcntl.h>


enum {
  reg_freq,
  reg_channels,
  reg_samples,
  reg_sbuf_size,
  reg_init,
  reg_count,
  reg_write,
  nr_reg
};

static uint8_t *sbuf = NULL;
static uint32_t *audio_base = NULL;
static uint8_t *read = NULL, *write = NULL;
static int ret;

static void calc_write_offset() {
  assert(write >= sbuf && write < sbuf + CONFIG_SB_SIZE);
  audio_base[reg_write] = (uint64_t)write - (uint64_t)sbuf;
}

static void parse_write_offset() {
  assert(audio_base[reg_write] < CONFIG_SB_SIZE);
  write = sbuf + audio_base[reg_write];
}

static void calc_count() {
  uint8_t *rp = read, *wp = write;
  audio_base[reg_count] = ((uint64_t) wp + CONFIG_SB_SIZE - (uint64_t) rp) % CONFIG_SB_SIZE;

}

static void audio_init() {
  read = sbuf;
  write = sbuf;
}

static void audio_read(void *userdata, Uint8 *stream, int len) {
  uint8_t *rp = read, *wp = write;
  if(rp <= wp) {
    size_t size = (uint64_t) wp - (uint64_t) rp;
    size = len < size? len : size;
    memcpy(stream, rp, size);
    memset(stream + size, 0, len - size);
    read = rp + size;
  }
  else {
    size_t size1 = (uint64_t)sbuf + CONFIG_SB_SIZE - (uint64_t)rp;
    if(len < size1) {
      memcpy(stream, rp, len);
      read = rp + size1;
      return;
    }
    size_t size2 = (uint64_t) wp - (uint64_t) sbuf;
    memcpy(stream, rp, size1);
    if(len < size1 + size2) {
      memcpy(stream + size1, sbuf, len - size1);
      read = sbuf + len;
      return;
    }
    memcpy(stream + size1, sbuf, size2);
    memset(stream + size1 + size2, 0, len - size1 - size2);
    read = sbuf + size2;
  }
}

static void audio_start() {
  SDL_AudioSpec s = {};
  s.format    = AUDIO_S16SYS;
  s.userdata  = NULL;
  s.freq      = audio_base[reg_freq];
  s.channels  = audio_base[reg_channels];
  s.samples   = audio_base[reg_samples];
  s.size      = audio_base[reg_sbuf_size];
  s.callback  = audio_read;
  SDL_OpenAudio(&s, NULL);
  SDL_PauseAudio(0);
}

static void audio_io_handler(uint32_t offset, int len, bool is_write) {
  if (ret != 0)
    return;
  if (offset == reg_init * sizeof(uint32_t) && is_write) {
    audio_init();
  }
  else if (offset == reg_samples * sizeof(uint32_t) && is_write) {
    audio_start();
  }
  else if (offset == reg_count * sizeof(uint32_t) && !is_write) {
    calc_count();
  }
  else if (offset == reg_write * sizeof(uint32_t) && !is_write) {
    calc_write_offset();
  }
  else if (offset == reg_write * sizeof(uint32_t) && is_write) {
    parse_write_offset();
  }

}

void init_audio() {
  ret = SDL_InitSubSystem(SDL_INIT_AUDIO);
  uint32_t space_size = sizeof(uint32_t) * nr_reg;
  audio_base = (uint32_t *)new_space(space_size);
  audio_base[reg_sbuf_size] = CONFIG_SB_SIZE - 1;
#ifdef CONFIG_HAS_PORT_IO
  add_pio_map ("audio", CONFIG_AUDIO_CTL_PORT, audio_base, space_size, audio_io_handler);
#else
  add_mmio_map("audio", CONFIG_AUDIO_CTL_MMIO, audio_base, space_size, audio_io_handler);
#endif
  sbuf = (uint8_t *)new_space(CONFIG_SB_SIZE);
  add_mmio_map("audio-sbuf", CONFIG_SB_ADDR, sbuf, CONFIG_SB_SIZE, NULL);
  audio_init();
}
